package game;

import java.util.HashSet;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import server.net.Client;
import shared.net.protocol.PacketCreator;

/**
 * Represents Nim players and provides methods for the various commands
 *
 * @author Andrew Gratta
 * @author Vincent Tsuei - Synchronization/Locking
 * @version 2012-11-28
 */
public class Player {

    // Player state fields.
    private GameInstance game;
    private String name;
    private PlayerState playerState;
    private ReentrantReadWriteLock playerStateLock;
    private Client client;
    
    // Invitation information
    private boolean allowInvites = true;
    private ReentrantReadWriteLock playerInviteLock;
    private HashSet<Player> playerInvitesTo; /*Invitations sent by this player*/
    private HashSet<Player> playerInvitesReceived; /*Invitations received by this player*/


    public Player(String name, Client client) {
        this.name = name;
        this.client = client;
        this.game = null;
        this.playerState = PlayerState.IN_LOBBY;
        this.playerStateLock = new ReentrantReadWriteLock();
        this.playerInviteLock = new ReentrantReadWriteLock();
        this.playerInvitesTo = new HashSet<Player>();
        this.playerInvitesReceived = new HashSet<Player>();
    }

    /**
     * Attempts to make a move in the current game. The player must be an active
     * participant, (not an observer) and it must be the player's turn
     *
     * @param n The number of items to remove
     * @param s The set to remove from
     * @return Returns true if the move was successful. Returns false if the
     * move is invalid or it is not the player's turn
     */
    public boolean makeMove(int n, int s) {
        if (game != null && game.getActivePlayer().equals(this)) {
            return game.makeMove(n, s, this);
        }
        return false;
    }

    /**
     * Get the username of the player
     *
     * @return
     */
    public String getName() {
        return name;
    }

    /**
     * Gets the game that the player is participating in (either observing or
     * playing)
     *
     * @return
     */
    public GameInstance getGame() {
        return game;
    }

    /**
     * Returns whether this player is waiting in the lobby.
     */
    public boolean isInLobby() {
        playerStateLock.readLock().lock();
        try {
            return playerState == PlayerState.IN_LOBBY;
        } finally {
            playerStateLock.readLock().unlock();
        }
    }

    /**
     * Returns whether this player is currently participating in a game.
     */
    public boolean isPlaying() {
        playerStateLock.readLock().lock();
        try {
            return playerState == PlayerState.PLAYING;
        } finally {
            playerStateLock.readLock().unlock();
        }
    }

    /**
     * Returns whether this player is observing a game.
     */
    public boolean isObserving() {
        playerStateLock.readLock().lock();
        try {
            return playerState == PlayerState.OBSERVING;
        } finally {
            playerStateLock.readLock().unlock();
        }
    }

    /**
     * Sets the player to observe the specified game, if the player is not
     * currently involved with another game. Notifies the game of a new
     * observer.
     *
     * @param game
     */
    public void observeGame(GameInstance game) {
        if (game != null) {
            playerStateLock.writeLock().lock();
            try {
                game.addObserver(this);
                this.game = game;
                playerState = PlayerState.OBSERVING;

            } finally {
                playerStateLock.writeLock().unlock();
            }
        }
    }

    /**
     * Removes the player from the game's list of observers and sets the
     * player's current status to InLobby
     */
    public void unobserveGame() {
        playerStateLock.writeLock().lock();
        try {
            if (game != null && playerState == PlayerState.OBSERVING) {
                GameInstance g = game;
                if (!game.isOver()) {
                    game.removeObserver(this);
                }
                game = null;
                playerState = PlayerState.IN_LOBBY;
                client.getConnection().queuePacketSend(PacketCreator.IN_GAME.getObserveGameEndNotify(g.getGameID()));
            }
        } finally {
            playerStateLock.writeLock().unlock();
        }
    }

    /**
     * Leaves the current game if the player is an active participant (not
     * observing) The game will end if it is not already over.
     */
    public void leaveGame() {
        playerStateLock.writeLock().lock();
        try {
            if (game != null && playerState == PlayerState.PLAYING) {
                if (!game.isOver()) {
                    game.setEndGame();
                }
                game = null;
                playerState = playerState.IN_LOBBY;
            }
        } finally {
            playerStateLock.writeLock().unlock();
        }
    }

    /**
     * Determines whether the player may currently make a move in the current
     * game
     *
     * @return Returns true if the player is participating in a game, and it is
     * their turn
     */
    public boolean canMakeMove() {
        if (game != null && game.getActivePlayer().equals(this)) {
            return true;
        }
        return false;
    }

    /**
     * Get the client associated with this player.
     */
    public Client getClient() {
        return client;
    }

    /**
     * Return whether this player has invited other
     */
    public boolean hasInvited(Player other) {
        this.playerInviteLock.readLock().lock();
        try {
            return this.playerInvitesTo.contains(other);
        } finally {
            this.playerInviteLock.readLock().unlock();
        }
    }

    /**
     * Return whether this player has been invite by other
     */
    public boolean hasBeenInvitedBy(Player other) {
        this.playerInviteLock.readLock().lock();
        try {
            return this.playerInvitesReceived.contains(other);
        } finally {
            this.playerInviteLock.readLock().unlock();
        }
    }

    /**
     * Invite the other player to a game. Returns whether the invite completed
     * successfully.
     */
    public boolean invite(Player other) {
        if (other == null) {
            throw new RuntimeException();
        }
        if (other.getName().equals(this.getName())) {
            //Inviting ourselves? ....
            throw new RuntimeException();
        }

        //We need to acquire locks on both players now.
        //To avoid deadlocking, define an absolute order based on name.
        final Player p1, p2;
        if (this.getName().compareTo(other.getName()) > 0) {
            p1 = this;
            p2 = other;
        } else {
            p1 = other;
            p2 = this;
        }
        p1.playerInviteLock.writeLock().lock();
        p2.playerInviteLock.writeLock().lock();
        try {

            //Check if invites are allowed.
            if (!this.allowInvites || !other.allowInvites) {
                return false;
            }

            //Check that both players are in lobby. - Acquire locks in order.
            p1.playerStateLock.readLock().lock();
            p2.playerStateLock.readLock().lock();
            try {
                if (p1.playerState != PlayerState.IN_LOBBY || p2.playerState != PlayerState.IN_LOBBY) {
                    return false;
                }
            } finally {
                p2.playerStateLock.readLock().unlock();
                p1.playerStateLock.readLock().unlock();
            }

            //Do some extra checks.
            if (p1.playerInvitesTo.contains(p2) //p1 invited p2 already
                    || p1.playerInvitesReceived.contains(p2) //p1 got an invite from p2 already
                    || p2.playerInvitesTo.contains(p1) //p2 invited p1 already
                    || p2.playerInvitesReceived.contains(p1)) { //p2 got an invite from p1 already
                return false; //An invite was already issued some way.
            }

            //Register invitations.
            this.playerInvitesTo.add(other);
            other.playerInvitesReceived.add(this);

            //Send out the invite notification.
            other.getClient().getConnection().queuePacketSend(PacketCreator.LOBBY.PLAY.getGameInviteNotify(this.getName()));

        } finally {
            p2.playerInviteLock.writeLock().unlock();
            p1.playerInviteLock.writeLock().unlock();
        }
        return true;
    }

    /**
     * Disable invitations to and from this player, then clear all remaining
     * ones. Must be called when logging out or disconnecting (otherwise, memory
     * leak is possible).
     */
    public void disableAndClearInvitations() {
        playerInviteLock.writeLock().lock();
        try {
            allowInvites = false;
        } finally {
            playerInviteLock.writeLock().unlock();
        }
        clearAllInvitations();
    }

    /**
     * Used to clear all invitations to and from this player.
     */
    public void clearAllInvitations() {
        clearInvitationsTo();
        clearInvitationsFrom();
    }

    /**
     * Clear invitations sent out by this player.
     */
    public void clearInvitationsTo() {
        //Grab thread-safe copy, then clear them.
        Player[] to;
        playerInviteLock.writeLock().lock();
        try {
            to = playerInvitesTo.toArray(new Player[0]);
            playerInvitesTo.clear();
        } finally {
            playerInviteLock.writeLock().unlock();
        }

        for (Player other : to) {
            other.playerInviteLock.writeLock().lock();
            try {
                other.playerInvitesReceived.remove(other);
            } finally {
                other.playerInviteLock.writeLock().unlock();
            }
        }
    }

    /**
     * Clear invitations received by this player.
     */
    public void clearInvitationsFrom() {
        //Grab thread-safe copy, then clear them.
        Player[] from;
        playerInviteLock.writeLock().lock();
        try {
            from = playerInvitesReceived.toArray(new Player[0]);
            playerInvitesReceived.clear();
        } finally {
            playerInviteLock.writeLock().unlock();
        }

        for (Player other : from) {
            other.playerInviteLock.writeLock().lock();
            try {
                other.playerInvitesTo.remove(other);
            } finally {
                other.playerInviteLock.writeLock().unlock();
            }
        }
    }

    /**
     * Return whether this client is allowed to send or accept invites.
     *
     * @return
     */
    public boolean canInvite() {
        playerInviteLock.readLock().lock();
        try {
            return this.allowInvites;
        } finally {
            playerInviteLock.readLock().unlock();
        }
    }

    /**
     * Accept an invite from the other player.
     */
    public boolean acceptInvite(Player other) {

        // Acquire locks in absolute order.
        final Player p1, p2;
        if (this.getName().compareTo(other.getName()) > 0) {
            p1 = this;
            p2 = other;
        } else {
            p1 = other;
            p2 = this;
        }

        p1.playerInviteLock.writeLock().lock();
        p2.playerInviteLock.writeLock().lock();
        try {

            //Check that invites were actually sent/received.
            if (other.playerInvitesTo.contains(this) && this.playerInvitesReceived.contains(other)) {

                //Check that both players are in lobby. - Acquire locks in order.
                p1.playerStateLock.writeLock().lock();
                p2.playerStateLock.writeLock().lock();
                try {
                    if (p1.playerState != PlayerState.IN_LOBBY || p2.playerState != PlayerState.IN_LOBBY) {
                        return false;
                    }

                    //Remove the invites and update the player states.
                    this.playerInvitesReceived.remove(other);
                    other.playerInvitesTo.remove(this);
                    this.playerState = PlayerState.PLAYING;
                    other.playerState = PlayerState.PLAYING;

                    //Extra assertion just in case
                    if (p1.game != null || p2.game != null) {
                        throw new RuntimeException("SRS ERROR.");
                    }

                    //Now create and register the game.
                    GameInstance newGame = new GameInstance(other, this);
                    p1.game = newGame;
                    p2.game = newGame;
                    this.getClient().getGameServer().getServerRegistry().registerGame(newGame);

                    // Tell clients about this.
                    this.getClient().getConnection().queuePacketSend(PacketCreator.LOBBY.PLAY
                            .getGameAcceptSuccess(newGame.getGameID(), other.getName(), newGame.getActivePlayer().equals(this),
                            newGame.getNumPiles(), newGame.getPiles()));
                    other.getClient().getConnection().queuePacketSend(PacketCreator.LOBBY.PLAY
                            .getGameStartNotify(newGame.getGameID(), this.getName(), newGame.getActivePlayer().equals(other),
                            newGame.getNumPiles(), newGame.getPiles()));

                    return true;
                } finally {
                    p2.playerStateLock.writeLock().unlock();
                    p1.playerStateLock.writeLock().unlock();
                }
            } else {
                return false;
            }
        } finally {
            p2.playerInviteLock.writeLock().unlock();
            p1.playerInviteLock.writeLock().unlock();
        }
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Player other = (Player) obj;
        return this.name.equals(other.name);
    }
}
